Skip to content

Fix[mqbs]: release proxy message refs only for pushed Apps - #1810

Draft
chrisbeard wants to merge 8 commits into
bloomberg:mainfrom
chrisbeard:fix/proxy-purge-refcount
Draft

chrisbeard wants to merge 8 commits into
bloomberg:mainfrom
chrisbeard:fix/proxy-purge-refcount

Conversation

@chrisbeard

@chrisbeard chrisbeard commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Fix proxy message reference accounting when an App did not receive a message.

A proxy counts references only for Apps included in a PUSH. However, every App slot initially has the pending e_PUT state, while only pushed Apps transition to e_PUSH. Two paths incorrectly treated any pending slot as holding a reference:

  • Removing an App's virtual storage (e.g. when an app loses its last reader)
  • Processing a CONFIRM from an App that was not pushed the message (e.g. after a proxy restart)

Either path could reduce the reference count below the number of Apps still holding the message. A later CONFIRM could then delete the message while another App still had it queued, resulting in silent message loss from a redelivery list or an assertion failure in PushStreamIterator.

Changes

  • Add VirtualStorageCatalog::holdsReference to distinguish proxy reference ownership:
    • On proxies, only e_PUSH holds a reference
    • On other nodes, any pending state holds a reference
  • Release references during virtual-storage removal only when the App holds one
  • Leave the reference count unchanged for CONFIRMs from unpushed Apps

Testing

Add unit coverage for:

  • Removing the virtual storage of an unpushed App
  • Confirming a message from an unpushed App

Add integration coverage for:

  • Graceful reader closure with a PushStream backlog
  • Reader failure with PushStream and redelivery backlogs
  • Reader failure with only a redelivery backlog
  • CONFIRM processing after a proxy restart

The integration tests verify that the proxy remains alive and that every remaining App receives its messages.

On a proxy, a message's refCount counts only the Apps it was pushed to,
but every App slot of a new DataStreamMessage starts as e_PUT, which
counts as pending.  Removing the virtual storage of an App that never
received the message still releases a reference, so a later CONFIRM
from one App reaches zero references while another App holds the
message.

This test reproduces that bug in the storage layer with minimal step.

Signed-off-by: Christopher Beard <cbeard9@bloomberg.net>
@chrisbeard

Copy link
Copy Markdown
Contributor Author

Test failure, as expected:

TEST /home/runner/work/blazingmq/blazingmq/src/groups/mqb/mqbs/mqbs_inmemorystorage.t.cpp CASE 22
Error /home/runner/work/blazingmq/blazingmq/src/groups/mqb/mqbs/mqbs_inmemorystorage.t.cpp(1501): 'storage.confirm(guid, k_APP_KEY1, 0)' (ZERO_REFERENCES) == 'mqbi::StorageResult::e_NON_ZERO_REFERENCES' (NON_ZERO_REFERENCES) (failed)
Error /home/runner/work/blazingmq/blazingmq/src/groups/mqb/mqbs/mqbs_inmemorystorage.t.cpp(1503): 'storage.confirm(guid, k_APP_KEY2, 0)' (NON_ZERO_REFERENCES) == 'mqbi::StorageResult::e_ZERO_REFERENCES' (ZERO_REFERENCES) (failed)

On a proxy, a message's refCount includes only the Apps named in its
PUSH. However, every App slot starts in e_PUT, which appears pending.

When an App loses its last reader, the proxy removes its virtual storage.
This wrongly releases references for messages that were never pushed to
that App. The refCount then falls below the number of Apps that still
hold the message.

A subsequent CONFIRM can consequently delete the message while another
App still needs it. If that App's copy is in the PushStream, the proxy
aborts when it tries to load the deleted message. If the copy is in a
redelivery list, the message is silently dropped.

Add integration coverage for three variants:

- An App closes while another App has a PushStream backlog.
- An App crashes while another App has both PushStream and redelivery
  backlogs.
- An App crashes while another App has only a redelivery backlog.

The tests verify that the proxy remains alive and that every remaining
App receives all its messages.

Note that three apps are required to trigger the bug, the proxy does not
assert for cases with only two Apps

Signed-off-by: Christopher Beard <cbeard9@bloomberg.net>
On a proxy, a message's refCount equals the number of Apps that received
that message in a PUSH.  The proxy marks exactly those Apps as 'e_PUSH'.
Every other App slot keeps the initial 'e_PUT', which counts as pending.
When an App lost its last reader, 'purgeImpl' released a reference for
every pending slot, so messages never pushed to that App lost a reference.
A later CONFIRM from one App then removed the message while another App
still had it queued, leading to silent message loss or an assert in
'PushStreamIterator'.

Add 'VirtualStorageCatalog::holdsReference' and release references
only for App slots that hold one.

Signed-off-by: Christopher Beard <cbeard9@bloomberg.net>
@dorjesinpo dorjesinpo added the bug Something isn't working label Oct 14, 2026
dorjesinpo
dorjesinpo previously approved these changes Oct 14, 2026
const bool holdsRef = holdsReference(
appMessageView(*dataStreamMessage, vs->ordinal()));

if (vs->remove(dataStreamMessage, replacingOrdinal) && holdsRef) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is important that remove gets called always.

@dorjesinpo

Copy link
Copy Markdown
Contributor

VirtualStorageCatalog::confirm needs similar fix.
check holdsReference and return e_NON_ZERO_REFERENCES if not e_PUSH

'remove' must run for every message, even when this App holds no
reference, so explicitly call it on its own instead of inside of an if
statement.

After looking more closely though, the check can be simplified to just
use 'holdsRef'; checking the return value of 'remove' is redundant.

Signed-off-by: Christopher Beard <cbeard9@bloomberg.net>
… other Apps' messages

After a proxy restarts, a consumer can confirm messages it was pushed
before the restart. If the restarted proxy was pushed those messages
for other Apps only, the CONFIRM from the unpushed App still releases a
reference, so a later CONFIRM from one of those Apps deletes a message
another App still has queued. This can cause a consumer to incorrectly
never receive the the message.

test_confirm_after_proxy_restart reproduces this bug using three Apps.

This test fails until the fix in a following commit.

Signed-off-by: Christopher Beard <cbeard9@bloomberg.net>
On a proxy, a message's refCount counts only the Apps it was pushed to,
but an App it was never pushed to keeps the initial 'e_PUT' slot, which
counts as pending.  A CONFIRM from that App still released a reference,
so a later CONFIRM from another App could reach zero references and
delete the message while a third App still held it.

This test reproduces the bug in the storage layer; it fails until the
following commit.

Signed-off-by: Christopher Beard <cbeard9@bloomberg.net>
On a proxy, a message's refCount is the number of Apps it was pushed to.
Those Apps are marked 'e_PUSH'; every other App slot keeps the initial
'e_PUT', which still counts as pending. 'VirtualStorageCatalog::confirm'
released a reference for any pending slot, so a CONFIRM from an App the
message was never pushed to still decremented the refCount. The refCount
could then reach zero, removing the message while another App still had
it queued, leading to silent message loss or an assert in
'PushStreamIterator'.

Check 'holdsReference' before confirming: when the App holds no
reference, return 'e_NON_ZERO_REFERENCES' and leave the refCount
unchanged.

This also corrects two unit tests whose proxy setup left counted Apps as
'e_PUT', which a real proxy does not do ('storePushIfProxy' marks every
counted App as 'e_PUSH').

Signed-off-by: Christopher Beard <cbeard9@bloomberg.net>
The new refCount unti tests used a local 'PooledBlobBufferFactory'
declared after the 'Tester' which then violated sanitizers upon
destruction.

Add the message with 'Tester::addMessages' instead, as the other tests
do.  Its blobs come from the mock cluster's buffer factory, which
outlives the storage, and its 'numPushApps' argument replaces the
hand-written 'put' and 'setPushState' calls.

Signed-off-by: Christopher Beard <cbeard9@bloomberg.net>
@chrisbeard chrisbeard changed the title UT[mqbs]: proxy VS removal must not release refs of unpushed Apps Fix[mqbs]: release message refs of Apps a message was pushed to on proxy Oct 21, 2026
@chrisbeard chrisbeard changed the title Fix[mqbs]: release message refs of Apps a message was pushed to on proxy Fix[mqbs]: release proxy message refs only for pushed Apps Oct 21, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants